package com.googlecode.jawb;

import java.lang.reflect.Field;

import com.googlecode.jawb.clues.types.DynamicLengthType;
import com.googlecode.jawb.clues.types.StaticLengthType;

/**
 * Uses {@link AnnotationParser} to determine required properties of fields and
 * calls appropriate callback (abstract) methods which can directly set or read
 * a given field.
 */
public abstract class Processor {
	protected   Object              processedObject     = null;
	protected   AnnotationParser    annotationParser    = null;
	private     ConverterFactory    converterFactory    = null;
	private     Exception           error               = null;
	private     boolean             kill                = false;
	
	/**
	 * @param processedObject
	 *            Object through which's fields it will iterate.
	 */
	public Processor(Object processedObject) {
		this.processedObject    = processedObject;
		this.annotationParser   = new AnnotationParser(processedObject);
		this.converterFactory   = new ConverterFactory();
	}
	
	/**
	 * Starts processing. From now on, callback methods will be called.
	 */
	public void process() {
		Field[] fields = processedObject.getClass().getDeclaredFields();
		
		for (Field field : fields) {
			if (kill) {
				return;
			}
			
			field.setAccessible(true);
			annotationParser.setField(field);

			if (annotationParser.isToSet()) {
				annotationParser.callPreVisitListener();

				if (annotationParser.isCustomStaticType()) {
					Class<? extends StaticLengthType> converterClass    = this.annotationParser.getStaticType();
					Object                            converterInstance = this.converterFactory.getInstance(converterClass);
					
					this.onStaticallyConvertedField(field, (StaticLengthType) converterInstance);
				} else if (annotationParser.isCustomDynamicType()) {
					Class<? extends DynamicLengthType> converterClass    = this.annotationParser.getDynamicType();
					Object                             converterInstance = this.converterFactory.getInstance(converterClass);
					
					this.onDynamicallyConvertedField(field, (DynamicLengthType) converterInstance);
				} else if (annotationParser.isValue()) {
					if (annotationParser.isPrimitiveValue()) {
						this.onPrimitiveField(field, annotationParser.getPrimitiveType());
					} else if (annotationParser.isObject()) {
						this.onObjectField(field, field.getType());
					}
				} else if (annotationParser.isArray()) {
					if (annotationParser.isPrimitiveValue()) {
						this.onPrimitiveArray(field, annotationParser.getArrayLength(), annotationParser.getPrimitiveType());
					} else if (annotationParser.isObject()) {
						this.onObjectArray(field, annotationParser.getArrayLength(), field.getType().getComponentType());
					}
				}
				
				annotationParser.callPostVisitListener();
			}
		}
	}

	/**
	 * An error has occurred. Since it's not possible to throw exception in the
	 * implementation of callback methods, this is the only way how to achieve
	 * the same effect.
	 */
	protected void setError(Exception error) {
		this.error = error;
	}
	
	/**
	 * @return
	 *      Exception which has occurred or null if any.
	 */
	protected Exception getError() {
		return this.error;
	}
	
	/**
	 * @return True if at least one error has occurred.
	 */
	public boolean hasError() {
		return this.error != null;
	}

	/**
	 * Immediately stops processing (as soon as possible, which means on the next
	 * iteration in the {@link #process()} method.
	 */
	protected void kill() {
		this.kill = true;
	}
	
	/**
	 * <p>Returns object which was processed (the same object which was passed to
	 * the constructor - {@link #Processor(Object)}).</p>
	 * 
	 * <p>Direct usage of this method is unsafe as it doesn't check if an error has
	 * occurred and doesn't throw an appropriate exception.</p>
	 * 
	 * @return Processed object.
	 */
	public Object getProcessedObject() {
		return this.processedObject;
	}
	
	/**
	 * Called when a field of primitive type is found.
	 * 
	 * @param field
	 *            Field of the class (might be accessed).
	 * @param asType
	 *            Requested type of the field.
	 */
	protected abstract void onPrimitiveField(Field field, BinaryType asType);
	
	/**
	 * Called when a field of {@link BinaryType#OBJECT} is found.
	 * 
	 * @param field
	 *            Field of the class (might be accessed).
	 * @param asClass
	 *            Requested type of the field.
	 */
	protected abstract void onObjectField(Field field, Class<?> asClass);
	
	/**
	 * Called when a field with an array of a primitive type is found.
	 * 
	 * @param field
	 *            Field of the class (might be accessed).
	 * @param length
	 *            Requested length of array.
	 * @param asType
	 *            Requested type of the field.
	 */
	protected abstract void onPrimitiveArray(Field field, int length, BinaryType asType);
	
	/**
	 * Called when a field with an array of a objects is found.
	 * 
	 * @param field
	 *            Field of the class (might be accessed).
	 * @param length
	 *            Requested length of array.
	 * @param asClass
	 *            Requested type of the field.
	 */
	protected abstract void onObjectArray(Field field, int length, Class<?> asClass);
	
	/**
	 * Called when a field with custom static-length converter is found.
	 * 
	 * @param field
	 *            Field of the class (might be accessed).
	 * 
	 * @param staticLengthType
	 *            Instance of a requested static-length converter.
	 */
	protected abstract void onStaticallyConvertedField(Field field, StaticLengthType staticLengthType);
	
	/**
	 * Called when a field with custom dynamic-length converter is found.
	 * 
	 * @param field
	 *            Field of the class (might be accessed).
	 * @param dynamicLengthType
	 *            Instance of a requested dynamic-length converter.
	 */
	protected abstract void onDynamicallyConvertedField(Field field, DynamicLengthType dynamicLengthType);
}
